/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
/**
 * @author Denis M. Kishenko
 */
package com.tom_roush.harmony.awt.geom;

import android.graphics.PointF;

import java.io.IOException;
import java.io.Serializable;

public class AffineTransform implements Cloneable, Serializable
{

    private static final long serialVersionUID = 1330973210523860834L;

    public static final int TYPE_IDENTITY = 0;
    public static final int TYPE_TRANSLATION = 1;
    public static final int TYPE_UNIFORM_SCALE = 2;
    public static final int TYPE_GENERAL_SCALE = 4;
    public static final int TYPE_QUADRANT_ROTATION = 8;
    public static final int TYPE_GENERAL_ROTATION = 16;
    public static final int TYPE_GENERAL_TRANSFORM = 32;
    public static final int TYPE_FLIP = 64;
    public static final int TYPE_MASK_SCALE = TYPE_UNIFORM_SCALE | TYPE_GENERAL_SCALE;
    public static final int TYPE_MASK_ROTATION = TYPE_QUADRANT_ROTATION | TYPE_GENERAL_ROTATION;

    /**
     * The <code>TYPE_UNKNOWN</code> is an initial type value
     */
    static final int TYPE_UNKNOWN = -1;

    /**
     * The min value equivalent to zero. If absolute value less then ZERO it considered as zero.
     */
    static final double ZERO = 1E-10;

    /**
     * The values of transformation matrix
     */
    double m00;
    double m10;
    double m01;
    double m11;
    double m02;
    double m12;

    /**
     * The transformation <code>type</code>
     */
    transient int type;

    public AffineTransform()
    {
        type = TYPE_IDENTITY;
        m00 = m11 = 1.0;
        m10 = m01 = m02 = m12 = 0.0;
    }

    public AffineTransform(AffineTransform t)
    {
        this.type = t.type;
        this.m00 = t.m00;
        this.m10 = t.m10;
        this.m01 = t.m01;
        this.m11 = t.m11;
        this.m02 = t.m02;
        this.m12 = t.m12;
    }

    public AffineTransform(float m00, float m10, float m01, float m11, float m02, float m12)
    {
        this.type = TYPE_UNKNOWN;
        this.m00 = m00;
        this.m10 = m10;
        this.m01 = m01;
        this.m11 = m11;
        this.m02 = m02;
        this.m12 = m12;
    }

    public AffineTransform(double m00, double m10, double m01, double m11, double m02, double m12)
    {
        this.type = TYPE_UNKNOWN;
        this.m00 = m00;
        this.m10 = m10;
        this.m01 = m01;
        this.m11 = m11;
        this.m02 = m02;
        this.m12 = m12;
    }

    public AffineTransform(float[] matrix)
    {
        this.type = TYPE_UNKNOWN;
        m00 = matrix[0];
        m10 = matrix[1];
        m01 = matrix[2];
        m11 = matrix[3];
        if (matrix.length > 4)
        {
            m02 = matrix[4];
            m12 = matrix[5];
        }
    }

    public AffineTransform(double[] matrix)
    {
        this.type = TYPE_UNKNOWN;
        m00 = matrix[0];
        m10 = matrix[1];
        m01 = matrix[2];
        m11 = matrix[3];
        if (matrix.length > 4)
        {
            m02 = matrix[4];
            m12 = matrix[5];
        }
    }

    /**
     * Creates an AffineTransform from and android.graphics.Matrix
     *
     * @param matrix the matrix to copy from
     */
    public AffineTransform(android.graphics.Matrix matrix)
    {
        float[] values = new float[9];
        matrix.getValues(values);
        m00 = values[0];
        m01 = values[1];
        m02 = values[2];
        m10 = values[3];
        m11 = values[4];
        m12 = values[5];
    }

    /*
     * Method returns type of affine transformation.
     *
     * Transform matrix is
     *   m00 m01 m02
     *   m10 m11 m12
     *
     * According analytic geometry new basis vectors are (m00, m01) and (m10, m11),
     * translation vector is (m02, m12). Original basis vectors are (1, 0) and (0, 1).
     * Type transformations classification:
     *   TYPE_IDENTITY - new basis equals original one and zero translation
     *   TYPE_TRANSLATION - translation vector isn't zero
     *   TYPE_UNIFORM_SCALE - vectors length of new basis equals
     *   TYPE_GENERAL_SCALE - vectors length of new basis doesn't equal
     *   TYPE_FLIP - new basis vector orientation differ from original one
     *   TYPE_QUADRANT_ROTATION - new basis is rotated by 90, 180, 270, or 360 degrees
     *   TYPE_GENERAL_ROTATION - new basis is rotated by arbitrary angle
     *   TYPE_GENERAL_TRANSFORM - transformation can't be inversed
     */
    public int getType()
    {
        if (type != TYPE_UNKNOWN)
        {
            return type;
        }

        int type = 0;

        if (m00 * m01 + m10 * m11 != 0.0)
        {
            type |= TYPE_GENERAL_TRANSFORM;
            return type;
        }

        if (m02 != 0.0 || m12 != 0.0)
        {
            type |= TYPE_TRANSLATION;
        }
        else if (m00 == 1.0 && m11 == 1.0 && m01 == 0.0 && m10 == 0.0)
        {
            type = TYPE_IDENTITY;
            return type;
        }

        if (m00 * m11 - m01 * m10 < 0.0)
        {
            type |= TYPE_FLIP;
        }

        double dx = m00 * m00 + m10 * m10;
        double dy = m01 * m01 + m11 * m11;
        if (dx != dy)
        {
            type |= TYPE_GENERAL_SCALE;
        }
        else if (dx != 1.0)
        {
            type |= TYPE_UNIFORM_SCALE;
        }

        if ((m00 == 0.0 && m11 == 0.0) ||
            (m10 == 0.0 && m01 == 0.0 && (m00 < 0.0 || m11 < 0.0)))
        {
            type |= TYPE_QUADRANT_ROTATION;
        }
        else if (m01 != 0.0 || m10 != 0.0)
        {
            type |= TYPE_GENERAL_ROTATION;
        }

        return type;
    }

    public double getScaleX()
    {
        return m00;
    }

    public double getScaleY()
    {
        return m11;
    }

    public double getShearX()
    {
        return m01;
    }

    public double getShearY()
    {
        return m10;
    }

    public double getTranslateX()
    {
        return m02;
    }

    public double getTranslateY()
    {
        return m12;
    }

    public boolean isIdentity()
    {
        return getType() == TYPE_IDENTITY;
    }

    public void getMatrix(double[] matrix)
    {
        matrix[0] = m00;
        matrix[1] = m10;
        matrix[2] = m01;
        matrix[3] = m11;
        if (matrix.length > 4)
        {
            matrix[4] = m02;
            matrix[5] = m12;
        }
    }

    public double getDeterminant()
    {
        return m00 * m11 - m01 * m10;
    }

    public void setTransform(double m00, double m10, double m01, double m11, double m02, double m12)
    {
        this.type = TYPE_UNKNOWN;
        this.m00 = m00;
        this.m10 = m10;
        this.m01 = m01;
        this.m11 = m11;
        this.m02 = m02;
        this.m12 = m12;
    }

    public void setTransform(AffineTransform t)
    {
        type = t.type;
        setTransform(t.m00, t.m10, t.m01, t.m11, t.m02, t.m12);
    }

    public void setToIdentity()
    {
        type = TYPE_IDENTITY;
        m00 = m11 = 1.0;
        m10 = m01 = m02 = m12 = 0.0;
    }

    public void setToTranslation(double mx, double my)
    {
        m00 = m11 = 1.0;
        m01 = m10 = 0.0;
        m02 = mx;
        m12 = my;
        if (mx == 0.0 && my == 0.0)
        {
            type = TYPE_IDENTITY;
        }
        else
        {
            type = TYPE_TRANSLATION;
        }
    }

    public void setToScale(double scx, double scy)
    {
        m00 = scx;
        m11 = scy;
        m10 = m01 = m02 = m12 = 0.0;
        if (scx != 1.0 || scy != 1.0)
        {
            type = TYPE_UNKNOWN;
        }
        else
        {
            type = TYPE_IDENTITY;
        }
    }

    public void setToShear(double shx, double shy)
    {
        m00 = m11 = 1.0;
        m02 = m12 = 0.0;
        m01 = shx;
        m10 = shy;
        if (shx != 0.0 || shy != 0.0)
        {
            type = TYPE_UNKNOWN;
        }
        else
        {
            type = TYPE_IDENTITY;
        }
    }

    public void setToRotation(double angle)
    {
        double sin = Math.sin(angle);
        double cos = Math.cos(angle);
        if (Math.abs(cos) < ZERO)
        {
            cos = 0.0;
            sin = sin > 0.0 ? 1.0 : -1.0;
        }
        else if (Math.abs(sin) < ZERO)
        {
            sin = 0.0;
            cos = cos > 0.0 ? 1.0 : -1.0;
        }
        m00 = m11 = cos;
        m01 = -sin;
        m10 = sin;
        m02 = m12 = 0.0;
        type = TYPE_UNKNOWN;
    }

    public void setToRotation(double angle, double px, double py)
    {
        setToRotation(angle);
        m02 = px * (1.0 - m00) + py * m10;
        m12 = py * (1.0 - m00) - px * m10;
        type = TYPE_UNKNOWN;
    }

    public static AffineTransform getTranslateInstance(double mx, double my)
    {
        AffineTransform t = new AffineTransform();
        t.setToTranslation(mx, my);
        return t;
    }

    public static AffineTransform getScaleInstance(double scx, double scY)
    {
        AffineTransform t = new AffineTransform();
        t.setToScale(scx, scY);
        return t;
    }

    public static AffineTransform getShearInstance(double shx, double shy)
    {
        AffineTransform m = new AffineTransform();
        m.setToShear(shx, shy);
        return m;
    }

    public static AffineTransform getRotateInstance(double angle)
    {
        AffineTransform t = new AffineTransform();
        t.setToRotation(angle);
        return t;
    }

    public static AffineTransform getRotateInstance(double angle, double x, double y)
    {
        AffineTransform t = new AffineTransform();
        t.setToRotation(angle, x, y);
        return t;
    }

    public void translate(double mx, double my)
    {
        concatenate(AffineTransform.getTranslateInstance(mx, my));
    }

    public void scale(double scx, double scy)
    {
        concatenate(AffineTransform.getScaleInstance(scx, scy));
    }

    public void shear(double shx, double shy)
    {
        concatenate(AffineTransform.getShearInstance(shx, shy));
    }

    public void rotate(double angle)
    {
        concatenate(AffineTransform.getRotateInstance(angle));
    }

    public void rotate(double angle, double px, double py)
    {
        concatenate(AffineTransform.getRotateInstance(angle, px, py));
    }

    /**
     * Multiply matrix of two AffineTransform objects
     *
     * @param t1 - the AffineTransform object is a multiplicand
     * @param t2 - the AffineTransform object is a multiplier
     * @return an AffineTransform object that is a result of t1 multiplied by matrix t2.
     */
    AffineTransform multiply(AffineTransform t1, AffineTransform t2)
    {
        return new AffineTransform(
            t1.m00 * t2.m00 + t1.m10 * t2.m01,          // m00
            t1.m00 * t2.m10 + t1.m10 * t2.m11,          // m01
            t1.m01 * t2.m00 + t1.m11 * t2.m01,          // m10
            t1.m01 * t2.m10 + t1.m11 * t2.m11,          // m11
            t1.m02 * t2.m00 + t1.m12 * t2.m01 + t2.m02, // m02
            t1.m02 * t2.m10 + t1.m12 * t2.m11 + t2.m12);// m12
    }

    public void concatenate(AffineTransform t)
    {
        setTransform(multiply(t, this));
    }

    public void preConcatenate(AffineTransform t)
    {
        setTransform(multiply(this, t));
    }

    public AffineTransform createInverse() throws NoninvertibleTransformException
    {
        double det = getDeterminant();
        if (Math.abs(det) < ZERO)
        {
            throw new NoninvertibleTransformException("Determinant is zero");
        }
        return new AffineTransform(
            m11 / det, // m00
            -m10 / det, // m10
            -m01 / det, // m01
            m00 / det, // m11
            (m01 * m12 - m11 * m02) / det, // m02
            (m10 * m02 - m00 * m12) / det  // m12
        );
    }

    public PointF transform(PointF src, PointF dst)
    {
        dst.set((float) (src.x * m00 + src.y * m01 + m02),
            (float) (src.x * m10 + src.y * m11 + m12));
        return dst;
    }

    public void transform(PointF[] src, int srcOff, PointF[] dst, int dstOff, int length)
    {
        while (--length >= 0)
        {
            PointF srcPoint = src[srcOff++];
            PointF dstPoint = dst[dstOff];
            if (dstPoint == null)
            {
                dstPoint = new PointF();
            }
            dstPoint.set((float) (srcPoint.x * m00 + srcPoint.y * m01 + m02),
                (float) (srcPoint.x * m10 + srcPoint.y * m11 + m12));
            dst[dstOff++] = dstPoint;
        }
    }

    public void transform(double[] src, int srcOff, double[] dst, int dstOff, int length)
    {
        int step = 2;
        if (src == dst && srcOff < dstOff && dstOff < srcOff + length * 2)
        {
            srcOff = srcOff + length * 2 - 2;
            dstOff = dstOff + length * 2 - 2;
            step = -2;
        }
        while (--length >= 0)
        {
            double x = src[srcOff + 0];
            double y = src[srcOff + 1];
            dst[dstOff + 0] = x * m00 + y * m01 + m02;
            dst[dstOff + 1] = x * m10 + y * m11 + m12;
            srcOff += step;
            dstOff += step;
        }
    }

    public void transform(float[] src, int srcOff, float[] dst, int dstOff, int length)
    {
        int step = 2;
        if (src == dst && srcOff < dstOff && dstOff < srcOff + length * 2)
        {
            srcOff = srcOff + length * 2 - 2;
            dstOff = dstOff + length * 2 - 2;
            step = -2;
        }
        while (--length >= 0)
        {
            float x = src[srcOff + 0];
            float y = src[srcOff + 1];
            dst[dstOff + 0] = (float) (x * m00 + y * m01 + m02);
            dst[dstOff + 1] = (float) (x * m10 + y * m11 + m12);
            srcOff += step;
            dstOff += step;
        }
    }

    public void transform(float[] src, int srcOff, double[] dst, int dstOff, int length)
    {
        while (--length >= 0)
        {
            float x = src[srcOff++];
            float y = src[srcOff++];
            dst[dstOff++] = x * m00 + y * m01 + m02;
            dst[dstOff++] = x * m10 + y * m11 + m12;
        }
    }

    public void transform(double[] src, int srcOff, float[] dst, int dstOff, int length)
    {
        while (--length >= 0)
        {
            double x = src[srcOff++];
            double y = src[srcOff++];
            dst[dstOff++] = (float) (x * m00 + y * m01 + m02);
            dst[dstOff++] = (float) (x * m10 + y * m11 + m12);
        }
    }

    public PointF deltaTransform(PointF src, PointF dst)
    {
        if (dst == null)
        {
            dst = new PointF();
        }

        dst.set((float) (src.x * m00 + src.y * m01), (float) (src.x * m10 + src.y * m11));
        return dst;
    }

    public void deltaTransform(double[] src, int srcOff, double[] dst, int dstOff, int length)
    {
        while (--length >= 0)
        {
            double x = src[srcOff++];
            double y = src[srcOff++];
            dst[dstOff++] = x * m00 + y * m01;
            dst[dstOff++] = x * m10 + y * m11;
        }
    }

    public PointF inverseTransform(PointF src, PointF dst) throws NoninvertibleTransformException
    {
        double det = getDeterminant();
        if (Math.abs(det) < ZERO)
        {
            throw new NoninvertibleTransformException("Determinant is zero");
        }

        if (dst == null)
        {
            dst = new PointF();
        }

        float x = (float) (src.x - m02);
        float y = (float) (src.y - m12);

        dst.set((float) ((x * m11 - y * m01) / det), (float) ((y * m00 - x * m10) / det));
        return dst;
    }

    public void inverseTransform(double[] src, int srcOff, double[] dst, int dstOff, int length)
        throws NoninvertibleTransformException
    {
        double det = getDeterminant();
        if (Math.abs(det) < ZERO)
        {
            throw new NoninvertibleTransformException("Determinant is zero");
        }

        while (--length >= 0)
        {
            double x = src[srcOff++] - m02;
            double y = src[srcOff++] - m12;
            dst[dstOff++] = (x * m11 - y * m01) / det;
            dst[dstOff++] = (y * m00 - x * m10) / det;
        }
    }

//    public Shape createTransformedShape(Shape src)
//    {
//        if (src == null)
//        {
//            return null;
//        }
//        if (src instanceof GeneralPath)
//        {
//            return ((GeneralPath) src).createTransformedShape(this);
//        }
//        PathIterator path = src.getPathIterator(this);
//        GeneralPath dst = new GeneralPath(path.getWindingRule());
//        dst.append(path, false);
//        return dst;
//    } TODO: PdfBox-Android

    @Override
    public String toString()
    {
        return getClass().getName() + "[[" + m00 + ", " + m01 + ", " + m02 + "], [" + m10 + ", " +
            m11 + ", " + m12 + "]]";
    }

    @Override
    public Object clone()
    {
        try
        {
            return super.clone();
        }
        catch (CloneNotSupportedException e)
        {
            throw new InternalError();
        }
    }

    @Override
    public boolean equals(Object obj)
    {
        if (obj == this)
        {
            return true;
        }
        if (obj instanceof AffineTransform)
        {
            AffineTransform t = (AffineTransform) obj;
            return
                m00 == t.m00 && m01 == t.m01 &&
                    m02 == t.m02 && m10 == t.m10 &&
                    m11 == t.m11 && m12 == t.m12;
        }
        return false;
    }

    /**
     * Write AffineTrasform object to the output steam.
     *
     * @param stream - the output stream
     * @throws IOException - if there are I/O errors while writing to the output strem
     */
    private void writeObject(java.io.ObjectOutputStream stream) throws IOException
    {
        stream.defaultWriteObject();
    }

    /**
     * Read AffineTransform object from the input stream
     *
     * @param stream - the input steam
     * @throws IOException - if there are I/O errors while reading from the input strem
     * @throws ClassNotFoundException - if class could not be found
     */
    private void readObject(java.io.ObjectInputStream stream)
        throws IOException, ClassNotFoundException
    {
        stream.defaultReadObject();
        type = TYPE_UNKNOWN;
    }

    /**
     * Returns this AffineTransform as an android.graphics.Matrix
     *
     * @return the matrix
     */
    public android.graphics.Matrix toMatrix()
    {
        android.graphics.Matrix retval = new android.graphics.Matrix();
        retval.setValues(new float[]{
            (float) m00, (float) m01, (float) m02,
            (float) m10, (float) m11, (float) m12,
            0.0f, 0.0f, 1.0f
        });
        return retval;
    }

    public class NoninvertibleTransformException extends java.lang.Exception
    {
        private static final long serialVersionUID = 6137225240503990466L;

        public NoninvertibleTransformException(String s)
        {
            super(s);
        }
    }
}
